import {DeviceTabs} from '@site/src/react-luma';
import {ShaderHooksExample} from '@site/src/examples';

# Shader Hooks

Shader hooks let shader modules inject code into designated points in a shader. This example defines an `OFFSET_POSITION` hook that two modules use to shift geometry left or right.

:::caution
Tutorials are maintained on a best-effort basis and may not be fully up to date (contributions welcome).
:::

<DeviceTabs />
<ShaderHooksExample />

It is assumed you've set up your development environment as described in [Setup](/docs/tutorials).

Hooks act like named callbacks within shader code. The base vertex shader calls
`OFFSET_POSITION`, and modules supply implementations that modify the position.
By attaching different modules to each model, one triangle is nudged left while
the other is pushed right.

The complete source for this example is shown below:

```typescript
import {Buffer, NumberArray, UniformStore} from '@luma.gl/core';
import {AnimationLoopTemplate, AnimationProps, Model} from '@luma.gl/engine';
import {ShaderAssembler} from '@luma.gl/shadertools';
import {webgl2Adapter} from '@luma.gl/webgl';
import {webgpuAdapter} from '@luma.gl/webgpu';

// Base vertex and fragment shader code
const vs = `\
#version 300 es

in vec2 position;

void main() {
  gl_Position = vec4(position, 0.0, 1.0);
  OFFSET_POSITION(gl_Position);
}
`;

const fs = `\
#version 300 es

uniform appUniforms {  
  vec3 color;
} app;

out vec4 fragColor;

void main() {
  fragColor = vec4(app.color, 1.0);
}
`;

const offsetLeftModule = {
  name: 'offsetLeft',
  inject: {
    'vs:OFFSET_POSITION': 'position.x -= 0.5;'
  }
};

const offsetRightModule = {
  name: 'offsetRight',
  inject: {
    'vs:OFFSET_POSITION': 'position.x += 0.5;'
  }
};

class AppAnimationLoopTemplate extends AnimationLoopTemplate {
  model1: Model;
  model2: Model;
  positionBuffer: Buffer;
  uniformBuffer1: Buffer;
  uniformBuffer2: Buffer;

  uniformStore = new UniformStore<{
    app: {
      color: NumberArray;
    };
  }>({
    app: {
      uniformTypes: {
        color: 'vec3<f32>'
      }
    }
  });

  constructor({device, animationLoop}: AnimationProps) {
    super();

    if (device.type !== 'webgl') {
      throw new Error('This demo is only implemented for WebGL2');
    }

    const shaderAssembler = ShaderAssembler.getDefaultShaderAssembler();
    shaderAssembler.addShaderHook('vs:OFFSET_POSITION(inout vec4 position)');

    this.positionBuffer = device.createBuffer(new Float32Array([-0.3, -0.5, 0.3, -0.5, 0.0, 0.5]));

    this.uniformBuffer1 = this.uniformStore.createUniformBuffer(device, 'app', {
      app: {
        color: [1, 0, 0]
      }
    });

    const uniformBufferData = this.uniformStore.getUniformBufferData('app');
    this.uniformBuffer1.write(uniformBufferData);

    this.uniformBuffer2 = this.uniformStore.createUniformBuffer(device, 'app', {
      app: {
        color: [0, 0, 1]
      }
    });

    this.model1 = new Model(device, {
      vs,
      fs,
      shaderAssembler, // Not needed, if not specified uses the default ShaderAssembler
      modules: [offsetLeftModule],
      bufferLayout: [{name: 'position', format: 'float32x2'}],
      attributes: {
        position: this.positionBuffer
      },
      vertexCount: 3,
      bindings: {
        app: this.uniformBuffer1
      }
    });

    this.model2 = new Model(device, {
      vs,
      fs,
      shaderAssembler, // Not needed, if not specified uses the default ShaderAssembler
      modules: [offsetRightModule],
      bufferLayout: [{name: 'position', format: 'float32x2'}],
      vertexCount: 3,
      attributes: {
        position: this.positionBuffer
      },
      bindings: {
        app: this.uniformBuffer2
      }
    });
  }

  onFinalize() {
    this.model1.destroy();
    this.model2.destroy();
    this.positionBuffer.destroy();
    this.uniformStore.destroy();
    this.uniformBuffer1.destroy();
    this.uniformBuffer2.destroy();
  }

  onRender({device}: AnimationProps) {
    const renderPass = device.beginRenderPass({clearColor: [0, 0, 0, 1]});
    this.model1.draw(renderPass);
    this.model2.draw(renderPass);
    renderPass.end();
  }
}

const animationLoop = makeAnimationLoop(AnimationLoopTemplate, {adapters: [webgpuAdapter, webgl2Adapter]})
animationLoop.start();
```

The `ShaderAssembler` gathers hook implementations from attached modules and
patches them into the base shader at build time. Hooks allow modules to customize
behavior without modifying original shader sources.
